CredentialsProvider
他是一個 next-auth
提供客製化帳密管理的功能搭配 next-auth
session 驗證機制,雖然 next-auth
官方並不推薦這種帳密管理方式,畢竟帳密安全這塊還是交給專業的平台把管比較實際,但撇除安全問題 CredentialsProvider
使用上蠻方便的,可以透過 callback
方式驗證 user
帳號,不需要再額外寫一個 api
去驗證。
首先我們先定義我們 signin
的 page
在哪邊因為本次login
跟 register
都在 /
,所以我們定義 signIn: '/'
,還記得我們昨日的 github
登入嗎,這邊定義好 signIn
後,日後只要呼叫 nextAuth
的 signIn()
,都會到 http://localhost:3000/
中。
大家還記得在實作 register
時候有添加的 prisma schema
嗎,他主要是用來搭配 adapter
用的,每當 user
登入成功就會自動把 user info
單加到你使用的 orm
資料中, next-auth
還有提供超多種的 orm
有興趣的讀者可以 參考參考 ~
import NextAuth, { AuthOptions, getServerSession } from "next-auth"
import CredentialsProvider from "next-auth/providers/credentials"
import GithubProvider from "next-auth/providers/github"
import { PrismaAdapter } from "@next-auth/prisma-adapter"
import { prisma } from "@/server/db"
export const authOptions: AuthOptions = {
//..
pages: { signIn: '/' },
adapter: PrismaAdapter(prisma),
}
export default NextAuth(authOptions)
補充一下
其實每個 adapter
只是拿以下的 code
把 client
傳下去執行的各種 callBack
而已,所以如果讀者想自己寫一個 adapter
可以拿以下的 code
去修改,但要記得 return
的 key
是固定的。
如果想知道其他 adpter
怎麼實作的可以參考官方寫法。
https://github.com/nextauthjs/next-auth/tree/main/packages
/** @return { import("next-auth/adapters").Adapter } */
export default function MyAdapter(client, options = {}) {
return {
async createUser(user) {
return
},
async getUser(id) {
return
},
async getUserByEmail(email) {
return
},
async getUserByAccount({ providerAccountId, provider }) {
return
},
async updateUser(user) {
return
},
async deleteUser(userId) {
return
},
async linkAccount(account) {
return
},
async unlinkAccount({ providerAccountId, provider }) {
return
},
async createSession({ sessionToken, userId, expires }) {
return
},
async getSessionAndUser(sessionToken) {
return
},
//..
}
}
https://next-auth.js.org/tutorials/creating-a-database-adapter
CredentialsProvider
主要有三個部分:name
: 讓 sign()
驗證你的 name
是什麼,假如你是 Credentials
,則可以透過signIn("Credentials")
告訴 next-auth
你要透過 CredentialsProvider
方式登入。
credentials
: 因為 CredentialsProvider
是一個需要客製化帳密的用法,這邊可以可你希望 user
輸入什麼 fields
。authorize
: 用來驗證 user auth
的 function
,而這邊 return
的結果將會是你希望 use
收到什麼的 info
到session
中。
以下一一介紹 authorize
中比較重要的內容。
import NextAuth, { AuthOptions, getServerSession } from "next-auth"
import CredentialsProvider from "next-auth/providers/credentials"
import bcrypt from 'bcrypt'
export const authOptions: AuthOptions = {
//..
providers: [
//..
CredentialsProvider({
name: 'Credentials',
credentials: {
email: { label: "email", type: "text", placeholder: "email" },
password: { label: "Password", type: "password" }
},
async authorize(credentials, req) {
if (!credentials?.email || !credentials?.password) {
throw new Error('Invalidate credentials')
}
const user = await prisma.user.findFirst({
where: {
email: credentials.email
}
})
if (!user || !user?.hashedPassword) {
throw new Error('user credentials not found or user have not register')
}
const isCorrectPassword = await bcrypt.compare(
credentials.password,
user.hashedPassword
)
if (!isCorrectPassword) {
throw new Error('Invalidate password')
}
return user
}
}),
// ...add more providers here
],
}
export default NextAuth(authOptions)
驗證 user
是否有 input
if (!credentials?.email || !credentials?.password) {
throw new Error('Invalidate credentials')
}
從 prisma
中尋找是否有 user
const user = await prisma.user.findFirst({
where: {
email: credentials.email
}
})
if (!user || !user?.hashedPassword) {
throw new Error('user credentials not found or user have not register')
}
驗證 user
是否有 hashedPassword
,同時透過bcrypt.compare
驗證 input password
,成功就 return user info
。
if (!user || !user?.hashedPassword) {
throw new Error('user credentials not found or user have not register')
}
const isCorrectPassword = await bcrypt.compare(
credentials.password,
user.hashedPassword
)
if (!isCorrectPassword) {
throw new Error('Invalidate password')
}![https://ithelp.ithome.com.tw/upload/images/20231004/20145677DfDTUT9TXN.png](https://ithelp.ithome.com.tw/upload/images/20231004/20145677DfDTUT9TXN.png)
return user
還記得我們昨天在用 github
登入時候查看 useSession
的內容嗎?你會發現這邊只有簡單的 user info
而已,但如果我們想要拿到其他 user info
呢?怎麼辦?,這時我們可以設定 next-auth
說你希望 return
什麼 data
到 session
中。
假設我希望將 user id
送到 session
,我們可以先將 callbacks
中的 jwt
從 prisma
中拿到 user
接著 return
,此時 session
中的params token
就是上面 jwt callback
return
的內容,這樣我們就可以return userId
到 session
中了。
export const authOptions: AuthOptions = {
//..
callbacks: {
async jwt({ token, user, account, profile, isNewUser }) {
const userInfo = await prisma.user.findFirst({
where: {
email: token.email
}
})
if (!userInfo) return token
return { ...token, ...userInfo }
},
session({ session, token }) {
return { ...session, user: { ...session.user, id: token.id } }
},
}
}
export default NextAuth(authOptions)
這時我們在看一下 console.log
,我們成功添加 userId
了~
最後放上完整 authOptions setting
如下
import NextAuth, { AuthOptions, getServerSession } from "next-auth"
import CredentialsProvider from "next-auth/providers/credentials"
import GithubProvider from "next-auth/providers/github"
import { PrismaAdapter } from "@next-auth/prisma-adapter"
import { prisma } from "@/server/db"
import bcrypt from 'bcrypt'
export const authOptions: AuthOptions = {
pages: { signIn: '/' },
adapter: PrismaAdapter(prisma),
// Configure one or more authentication providers
providers: [
GithubProvider({
clientId: process.env.GITHUB_ID as string,
clientSecret: process.env.GITHUB_SECRET as string,
}),
CredentialsProvider({
// The name to display on the sign in form (e.g. 'Sign in with...')
name: 'Credentials',
// The credentials is used to generate a suitable form on the sign in page.
// You can specify whatever fields you are expecting to be submitted.
// e.g. domain, username, password, 2FA token, etc.
// You can pass any HTML attribute to the <input> tag through the object.
credentials: {
email: { label: "email", type: "text", placeholder: "email" },
password: { label: "Password", type: "password" }
},
async authorize(credentials, req) {
if (!credentials?.email || !credentials?.password) {
throw new Error('Invalidate credentials')
}
const user = await prisma.user.findFirst({
where: {
email: credentials.email
}
})
if (!user || !user?.hashedPassword) {
throw new Error('user credentials not found or user have not register')
}
const isCorrectPassword = await bcrypt.compare(
credentials.password,
user.hashedPassword
)
if (!isCorrectPassword) {
throw new Error('Invalidate password')
}
return user
}
}),
// ...add more providers here
],
secret: process.env.AUTH_SECRET,
callbacks: {
async jwt({ token, user, account, profile, isNewUser }) {
const userInfo = await prisma.user.findFirst({
where: {
email: token.email
}
})
if (!userInfo) return token
return { ...token, ...userInfo }
},
session({ session, token }) {
return { ...session, user: { ...session.user, id: token.id } }
},
},
session: {
strategy: 'jwt',
maxAge: 1 * 24 * 60 * 60
}
}
export default NextAuth(authOptions)
之後我們到 authForm
在 onSubmit
中把 signin
邏輯加上,那 signIn
哪有一個 callBack
結果,我們可以透過 callBack
去做一些登入成功或是失敗的行為。
const onSubmit = async (value: LoginFormSchema | RegisterFormSchema) => {
try {
setIsLoading(true)
if (variants === 'Login') {
const callBack = await signIn('credentials', {
email: value.email,
password: value.password,
redirect: false
})
if (callBack?.error) {
toast.error(callBack.error)
}
if (callBack?.ok) {
toast.success('success login')
router.push('/posts')
}
}
Register
部分我們也可以優化一下,只要 user
註冊成功我們就自動幫忙 login
。
const onSubmit = async (value: LoginFormSchema | RegisterFormSchema) => {
try {
setIsLoading(true)
if (variants === 'Login') {
//..
}
if (formSchemaTypeGuard(value) && variants === 'Register') {
const { message } = await axios.post<{ message: string }, AxiosResponse<{ message: string }>, RegisterSchema>('/api/register', {
name: value.name,
email: value.email,
password: value.password,
}).then(res => res.data)
toast.success(message)
const callBack = await signIn('credentials', {
email: value.email,
password: value.password,
redirect: false
})
if (callBack?.error) {
toast(callBack.error)
}
if (callBack?.ok) {
toast.success('success login')
router.push('/posts')
}
}
} catch (e) {
if (e instanceof AxiosError) {
const message = e.response?.data.message
toast.error(message)
return
}
console.log(e)
} finally {
setIsLoading(false)
}
}
因為login
跟 register
共用同一個 signin
邏輯所以拆成 handleSignCredentials
const handleSignCredentials = async (value: LoginFormSchema | RegisterFormSchema) => {
const callBack = await signIn('credentials', {
email: value.email,
password: value.password,
redirect: false
})
if (callBack?.error) {
toast(callBack.error)
}
if (callBack?.ok) {
toast.success('success login')
router.push('/posts')
}
}
如此code
就簡潔多了~
const onSubmit = async (value: LoginFormSchema | RegisterFormSchema) => {
try {
setIsLoading(true)
if (variants === 'Login') {
handleSignCredentials(value)
}
if (formSchemaTypeGuard(value) && variants === 'Register') {
const { message } = await axios.post<{ message: string }, AxiosResponse<{ message: string }>, RegisterSchema>('/api/register', {
name: value.name,
email: value.email,
password: value.password,
}).then(res => res.data)
toast.success(message)
handleSignCredentials(value)
}
} catch (e) {
if (e instanceof AxiosError) {
const message = e.response?.data.message
toast.error(message)
return
}
console.log(e)
} finally {
setIsLoading(false)
}
}
之後我們測試一下結果,如果讀者看動同一個畫面,恭喜你成功勒~
到這邊終於把 Next-auth
的主要功能介紹一遍了~明天我們就可以用 next-auth
的 session
結合 trpc
做身份驗證了~
https://github.com/nextauthjs/next-auth/tree/main/packages
https://next-auth.js.org/tutorials/creating-a-database-adapter
https://next-auth.js.org/adapters
https://github.com/Danny101201/next_demo/tree/main
✅ 前端社群 :
https://lihi3.cc/kBe0Y